﻿using System;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;

namespace NanoMvvm
{
    public class WeakFuncContainer<TParam, TResult> : IWeakFunctor<TParam, TResult>, IEquatable<WeakFuncContainer<TParam, TResult>>, IEquatable<Func<TParam, TResult>>
    {
        private delegate TR OpenFunc<T, TR>(object instance, T param);

        private readonly Func<TParam, TResult> staticInvoker;
        private readonly OpenFunc<TParam, TResult> instanceInvoker;
        private readonly MethodInfo instanceMethodInfo;
        private readonly WeakReference<object> target;

        private readonly bool isStatic;

        public WeakFuncContainer(Func<TParam, TResult> func)
        {
            if (func == null) throw new ArgumentNullException("func");
            if (func.Method.DeclaringType.IsValueType) throw new ArgumentException("Declaring type must be a reference type.");

            if(func.Target == null)
            {
                Debug.Assert(func.Method.IsStatic);

                isStatic = true;
                staticInvoker = func;
            }
            else
            {
                Debug.Assert(!func.Method.IsStatic);

                target = new WeakReference<object>(func.Target);

                instanceInvoker = CreateOpenFuncDelegate(func);

                instanceMethodInfo = func.Method;
            }
        }

        public TResult Compute(TParam parameter)
        {
            if (isStatic)
            {
                return staticInvoker(parameter);
            }

            if (target.IsTargetAlive)
            {
                return instanceInvoker(target.Target, parameter);
            }

            throw new InvalidOperationException("Delegate target instance garbage collected before call.");
        }

        public bool IsTargetAlive
        {
            get { return isStatic || target.IsTargetAlive; }
        }

        private static OpenFunc<T, TR> CreateOpenFuncDelegate<T, TR>(Func<T, TR> func)
        {
            Debug.Assert(func != null);
            Debug.Assert(!func.Method.IsStatic);

            MethodInfo method = func.Method;
            Type type = typeof(T);

            ParameterExpression instanceExpression = Expression.Parameter(typeof(object));
            ParameterExpression paramExpression = Expression.Parameter(type);

            Expression<OpenFunc<T, TR>> expression = Expression.Lambda<OpenFunc<T, TR>>(
                Expression.Call(
                    Expression.Convert(instanceExpression, func.Method.DeclaringType),
                    method,
                    paramExpression
                ),
                instanceExpression,
                paramExpression
            );

            return expression.Compile();
        }

        public override int GetHashCode()
        {
            return isStatic ? staticInvoker.GetHashCode() : instanceInvoker.GetHashCode();
        }

        public override bool Equals(object other)
        {
            if (ReferenceEquals(null, other)) return false;

            var wfc = other as WeakFuncContainer<TParam, TResult>;
            if (!ReferenceEquals(null, wfc))
            {
                return Equals(wfc);
            }

            var f = other as Func<TParam>;
            if (!ReferenceEquals(null, f))
            {
                return Equals(f);
            }

            return false;
        }

        public bool Equals(Func<TParam, TResult> other)
        {
            if (ReferenceEquals(null, other)) return false;

            if (other.Target == null)
            {
                Debug.Assert(other.Method.IsStatic);

                return isStatic && other.Method.Equals(staticInvoker.Method);
            }

            Debug.Assert(!other.Method.IsStatic);

            return !isStatic && target.IsTargetAlive &&
                   other.Method.Equals(instanceMethodInfo) &&
                   ReferenceEquals(other.Target, target.Target);
        }

        public bool Equals(WeakFuncContainer<TParam, TResult> other)
        {
            if (ReferenceEquals(null, other)) return false;

            if (other.isStatic)
            {
                return isStatic && other.staticInvoker.Equals(staticInvoker);
            }

            return !isStatic && target.IsTargetAlive && //other.target.IsTargetAlive &&
                   other.instanceMethodInfo.Equals(instanceMethodInfo) &&
                   ReferenceEquals(other.target.Target, target.Target);
        }
    }
}